Skip to content

Linux support: build, runtime, and importer fixes (177/177 tests passing)#46

Merged
jonathanembleyriches merged 13 commits into
URLab-Sim:mainfrom
vivekgr92:linux-support
Apr 30, 2026
Merged

Linux support: build, runtime, and importer fixes (177/177 tests passing)#46
jonathanembleyriches merged 13 commits into
URLab-Sim:mainfrom
vivekgr92:linux-support

Conversation

@vivekgr92
Copy link
Copy Markdown
Contributor

Summary

  • Adds Linux support to URLab against UE 5.7 binary distribution: 6 commits, 177/177 of the URLab.* automation tests passing on Linux from a clean rebuild.
  • Fixes a real (Windows-too) latent bug in default-class import (TCHAR_TO_UTF8 macro output stored in a long-lived const char*) that was hidden on MSVC by stack-reuse luck and surfaced only on Linux/clang.
  • Build + runtime + docs only — no Blueprint or .uasset changes, no Windows behaviour changes.

Motivation

Fixes #45. The README hedged "Linux is experimental"; in practice URLab didn't compile cleanly on Linux against UE's bundled clang and didn't load at runtime even after building. With these patches the editor launches cleanly without LD_LIBRARY_PATH, the MJCF importer registers, and the full automation suite passes.

Linked issue

Fixes #45

Build + test evidence

=== URLab build+test summary ===
Timestamp : 2026-04-28 02:20:22 UTC
Git HEAD  : 6a125d9d (linux-support)
Engine    : UE 5.7.4 (Linux binary distribution)
Build     : Succeeded
Tests     : 177 / 177 passed (0 failed)  [177 tests performed]
Log       : sha256:f225685ab13fd5a9 (4.8 MB; available on request)
================================

Run via ./Scripts/build_and_test_linux.sh --engine $UE_ROOT --project /path/to/HostProject.uproject. Editor closed beforehand. LD_LIBRARY_PATH unset (RPATH staging from commit f7b8cb1d makes it unnecessary).

What changed, by commit

  1. fix(linux): make URLab plugin build and dlopen on UE Linux (876048b)
    Six small build-time blockers: _WIN32=0 define on Linux (forced MuJoCo down the __declspec(dllimport) branch), URLab.h _WIN32 redefine fired on every platform, a tcp://*:5558 doc comment that tripped -Wcomment, a Coacd/coacd.h include with the wrong case, and LoadDependencyDLL / GetDllHandle calls hardcoded to mujoco.dll. All gated on PLATFORM_WINDOWS / PLATFORM_LINUX so the Windows path is unchanged.

  2. feat(linux): add Scripts/build_and_test_linux.sh (ea4738c)
    Linux-platform parallel of build_and_test.sh. Same summary-block format. Uses Build.sh + UnrealEditor-Cmd (existing script hardcodes Win64 .exe paths, so the Linux invocation in CONTRIBUTING.md would currently fail — see open question 2 below).

  3. fix(linux): keep TCHAR_TO_UTF8 conversions alive across mjs_* calls (312b077)
    Root cause of the 5 Linux test failures from the original 172/177 run: TCHAR_TO_UTF8(*FString) returns a pointer into a temporary FTCHARToUTF8 whose lifetime ends at the full expression. MjSpecWrapper::AddDefault and MjTendon::RegisterToSpec stored that pointer in long-lived const char* variables; on MSVC the freed stack still held the original bytes by coincidence, on clang/Linux the stack was reclaimed and mjs_addDefault / mjs_wrapGeom got garbage class names. Fix is FTCHARToUTF8 with explicit function-scope lifetime — ~4 lines total. Tests jumped from 172/177 to 177/177.

  4. feat(linux): stage third-party libs into Binaries/Linux/ for $ORIGIN RPATH (f7b8cb1)
    UBT's auto-computed RPATH for plugins not living inside the host project tree resolves to invented /home/<user>/<project>/... paths that don't exist. Sidestep that by symlinking third_party/install/<pkg>/lib/*.so* into the plugin's own Binaries/Linux/ directory, where ${ORIGIN} (which UBT does add correctly) finds them. New helper: Scripts/setup_runtime_linux.sh (idempotent). build_and_test_linux.sh invokes it after build. After this patch the editor launches with LD_LIBRARY_PATH unset.

  5. build(linux): apply CoACD _WIN32 patch via CoACD_custom overlay; drop libzmq.a (6a125d9)
    Replaces a direct edit of CoACD's submodule header with an overlay file at third_party/CoACD_custom/public/coacd.h, copied onto src/public/ by the existing build-script overlay pattern (build.sh and build.ps1 both updated). Also disables libzmq's static archive on Linux (BUILD_STATIC=OFF) — the static mailbox_safe.cpp pulls libc++'s pthread_cond_clockwait which UE's link sysroot can't resolve; the .so works fine and is what the plugin actually uses.

  6. docs: add Linux setup guide and drop the experimental hedge (fc94bb1)
    New docs/linux_setup.md walks through prerequisites, why UE's bundled clang + libc++ require an env-var sandwich for the third-party builds, the runtime staging step, and how to run the automation suite. README updated to point at it; mkdocs nav updated.

Open questions / decisions made (with reasoning)

  1. Build-script split: parallel _linux.sh, not unified. Per your reply on Linux support: build, runtime, and importer issues (172/177 tests passing) #45 ("A parallel is good for now"). I did not collapse build_all.sh and build_and_test.sh into platform-detecting versions — happy to do that as a follow-up if you'd prefer; it's a separate, larger change and would touch the existing Windows scripts. But the existing build_and_test.sh is currently hardcoded to Win64 binary names and platform tag, so any Linux user following CONTRIBUTING.md verbatim would fail. Worth flagging in case you want me to fix build_and_test.sh itself in this PR or a follow-up.

  2. CoACD overlay, not upstream PR. Per your reply on Linux support: build, runtime, and importer issues (172/177 tests passing) #45. Happy to also send an upstream PR to https://github.com/SarahWeiii/CoACD for the _WIN32 fix if you want — say the word and I'll do it after this PR lands.

  3. RPATH approach. Investigated UBT's auto-RPATH and found it produces a path like ${ORIGIN}/../../../../../UnrealEngine/../URLabTest/Plugins/UnrealRoboticsLab/third_party/install/MuJoCo/lib that resolves to /home/<user>/URLabTest/... (with the <user> segment dropped) — broken for any plugin not living inside the host project tree. The symlinks-into-${ORIGIN} approach in commit 4 sidesteps that entirely and works for both editor builds and a packaged plugin (since RuntimeDependencies with $(BinaryOutputDir) target also stages the libs there at packaging time). If you'd prefer a different convention I'll switch.

  4. urlab_bridge not tested on Linux yet. Out of scope for this PR (separate repo). Mentioned as a caveat in docs/linux_setup.md.

Manual verification steps

  1. Clone linux-support branch into your project's Plugins/ (or symlink in).
  2. From the plugin root, git submodule update --init --recursive.
  3. Build third-party libs with the env-var sandwich from docs/linux_setup.md (UE's clang + -stdlib=libc++ etc.).
  4. Engine/Build/BatchFiles/Linux/Build.sh <YourProject>Editor Linux Development -Project=...
  5. ./Scripts/setup_runtime_linux.sh (or use Scripts/build_and_test_linux.sh which calls it for you).
  6. Launch UnrealEditor against your project — URLab plugin should load with no LogURLab: Error: ... not found lines.
  7. Drag any MJCF (e.g. third_party/MuJoCo/src/model/humanoid/humanoid.xml) into Content Browser → MJCF importer should fire.

Checklist

  • Builds locally against UE 5.7+
  • Full URLab.* automation suite passes (177/177)
  • Docs updated for user-facing changes (README.md, new docs/linux_setup.md, mkdocs.yml)

@jonathanembleyriches
Copy link
Copy Markdown
Contributor

Awesome! Thanks for this.

Pulled it locally (and set up a linux test project) and it works end-to-end: 177/177 tests passing, editor launches cleanly without LD_LIBRARY_PATH.

Could you rebase onto main before a final review? There's concrete overlap with main since the branch diverged (we cleaned up some of the builds prior:

  • e6d8f564 modifies all three per-dep build.{sh,ps1} and URLab.Build.cs for stale-install detection — the same files this PR touches for the Linux changes.
  • 85092a51 and 51cce72a modify Scripts/build_and_test.sh (the script build_and_test_linux.sh was forked from), so the Linux fork will likely want those updates mirrored after the rebase.

Easier to review on top of current main than to guess at the merge resolution (everything looks great already though).

A few things came up while testing - some potential changes (happy to discuss):

  1. Fold the env-var sandwich into build_all.sh so the user never sets it themselves. A wrong toolchain silently produces unlinkable GCC LTO bitcode rather than a real .so (cost us a couple of debugging cycles to spot). Concrete proposal: build_all.sh --engine $UE_ROOT, glob the Linux_x64/v*_clang-* toolchain dir so it survives UE bumps, inject the env vars internally. The doc collapses to one command.

  2. Trigger setup_runtime_linux.sh automatically when a single third-party dep is updated. If someone bumps one submodule SHA and runs only that dep's build.sh directly (skipping build_all.sh / build_and_test_linux.sh), Binaries/Linux/ keeps the stale symlinks and the editor fails to load. Cleanest fix is calling setup_runtime_linux.sh at the end of each per-dep build.sh (Linux only), with the script becoming warn-and-skip when Binaries/Linux/ doesn't exist yet so first-time-fresh-checkout still works.

  3. The new CoACD_custom/public/coacd.h overlay is a full-file copy for a small change (#if _WIN32#if defined(_WIN32) on line 10). Since submodule SHAs are pinned there's no silent drift risk; the only cost is having to re-merge the full file against upstream the next time the CoACD pin moves. Could you go ahead with the upstream PR you offered in open question 2? Once that lands, the overlay can be dropped entirely on the next CoACD pin bump.

  4. Flag for a follow-up PR (or happy to be collapsed into this one): docs for getting UE itself running on Linux. docs/linux_setup.md assumes a working $UE_ROOT, but landing there from a fresh distro (Epic Launcher / Setup.sh dependencies, etc.) is not well documented online. A short prerequisites section or a separate docs/unreal_linux_install.md linked from there would smooth the path for new users.

Let me know your thoughts, and thanks again!

Six small Linux-only changes that unblock building the plugin against
UE 5.7's Linux toolchain (clang 20 + libc++) and let it locate
MuJoCo / CoACD / libzmq at runtime.

- URLab.Build.cs: drop _WIN32=0 / __linux__=1 defines on Linux. With
  _WIN32 defined (even as 0), MuJoCo's mjexport.h takes the
  __declspec(dllimport) branch, which clang on Linux can't parse.
  __linux__ / __unix__ are already defined by the compiler.
- URLab.h: gate the _WIN32 redefine block on PLATFORM_WINDOWS so it
  no longer fires on Linux. Same root cause as Build.cs above.
- MjCamera.h: replace tcp://*:5558 with tcp://0.0.0.0:5558 in a
  /** doc comment **/ — the literal `://*` triggers -Wcomment which
  is -Werror in UE's build.
- CoacdInterface.h: case-correct include "Coacd/coacd.h" -> "CoACD/coacd.h"
  to match the installed header path. Linux fs is case-sensitive.
- URLab.cpp + MjPhysicsEngine.cpp: branch the third-party shared-lib
  names on PLATFORM_WINDOWS / PLATFORM_LINUX so dlopen can find them
  by their actual SONAMEs (libmujoco.so.3.7.0, lib_coacd.so,
  libzmq.so.5) and the per-package install/<pkg>/lib subdirectory.

With these patches the URLab.* automation suite on Linux runs to
172/177 passing. The 5 remaining failures cluster on a separate
MJCF default-class inheritance issue tracked in URLab-Sim#45.

Refs: URLab-Sim#45
Mirrors build_and_test.sh's summary-block format but uses the Linux
UnrealBuildTool / UnrealEditor-Cmd binaries, builds the host project's
Editor target on the Linux platform, and exports LD_LIBRARY_PATH at
the third-party install dirs (RuntimeDependencies aren't staged on
Linux editor builds, so the plugin's libmujoco / lib_coacd / libzmq
aren't otherwise dlopen-able).

Note: the existing build_and_test.sh hardcodes Win64 binary names and
a Win64 build platform, so the Linux invocation in CONTRIBUTING.md
would currently fail. Whether this should be a parallel _linux.sh or
a unified platform-detecting script is an open question; see URLab-Sim#45.

Refs: URLab-Sim#45
URLab stored TCHAR_TO_UTF8 macro output in a long-lived `const char*`,
which dangles immediately because the macro expands to a pointer into
a temporary FTCHARToUTF8 whose lifetime ends at the end of the full
expression.

On Windows/MSVC the stack memory often still held the original bytes
when the pointer was used a few lines later, so the bug went
unnoticed. On Linux/clang the stack is reclaimed aggressively and the
pointer dereferences garbage. Two consequences observed on Linux:

- FMujocoSpecWrapper::AddDefault registered each <default> class
  under a corrupted name, so subsequent mjs_findDefault lookups by
  the joint/geom/actuator import paths failed silently and MuJoCo
  fell back to its built-in defaults. This caused all five
  Linux-only test failures from the previous run:
    URLab.Import.DefaultClassJointAxis
    URLab.Import.DefaultFromTo
    URLab.Import.RoundTrip_Defaults
    URLab.Muscle.Arm26_ActuatorParams
    URLab.Muscle.Arm26_Counts
  After this fix all 177 URLab.* tests pass on Linux.

- UMjTendon::RegisterToSpec did the same thing with the side-site
  string for <tendon><geom .../></tendon> wrapping. No failing test
  caught it but the pattern is identical so it's fixed here too.

Both call sites now construct an explicit FTCHARToUTF8 with
function-scope lifetime so the converted bytes outlive the
mjs_addDefault / mjs_wrapGeom call.

Refs: URLab-Sim#45
…RPATH

UBT's auto-computed RPATH for the URLab plugin .so on Linux resolves
incorrectly when the plugin lives outside the host project tree (e.g.
symlinked in from a separate clone). The relative path it emits is

  ${ORIGIN}/../../../../../UnrealEngine/../URLabTest/Plugins/.../lib

which resolves to a non-existent /home/<user>/URLabTest/... rather than
the plugin's real on-disk location. Result: the loader can't find
libmujoco / lib_coacd / libzmq at editor startup, so the plugin module
fails to load without LD_LIBRARY_PATH.

Side-step the relative-path issue by symlinking third-party shared libs
into the plugin's own Binaries/Linux/ directory after build. UBT already
adds ${ORIGIN} to the plugin .so's RPATH (correctly, this time), so the
loader finds the libs in the same dir as the plugin .so. No
LD_LIBRARY_PATH needed at editor or packaging time.

Changes:
- URLab.Build.cs (Linux branch): drop the broken
  PublicRuntimeLibraryPaths.Add(<absolute path>) entries (they end up
  generating bad RPATH); use RuntimeDependencies with $(BinaryOutputDir)
  target so packaging stages the libs alongside the plugin .so. Glob
  *.so* (not just *.so) so SONAME-versioned real files like
  libmujoco.so.3.7.0 also get staged.
- Scripts/setup_runtime_linux.sh: new helper that symlinks
  third_party/install/<pkg>/lib/*.so* into Binaries/Linux/ for editor
  builds (where RuntimeDependencies aren't auto-staged on Linux).
  Idempotent.
- Scripts/build_and_test_linux.sh: invoke setup_runtime_linux.sh after
  the plugin builds, drop the LD_LIBRARY_PATH fallback (no longer
  needed).

After: editor launches and the URLab.* automation suite runs to
177/177 passing with LD_LIBRARY_PATH unset.

Refs: URLab-Sim#45
… libzmq.a

Stop directly editing the CoACD submodule's public/coacd.h. Instead
overlay a fixed copy via the existing CoACD_custom/ pattern that
URLab already uses for CMakeLists.txt + cmake/. The fixed copy
relaxes `#if _WIN32` to `#if defined(_WIN32)` so consumers compiled
with `-Wundef -Werror` (UE on Linux) accept the header without
warnings becoming errors. Both build.sh and build.ps1 grow a
parallel block to copy CoACD_custom/public/* onto src/public/.

Also disable libzmq's static archive on Linux (BUILD_STATIC=OFF).
The static archive's mailbox_safe.cpp pulls libc++
condition_variable_any::wait_until -> pthread_cond_clockwait via
inlining, which UE's link sysroot can't resolve. The .so version
links its own runtime deps internally and works fine, so the static
lib is unused and only causes the URLab plugin link to fail
(undefined symbol pthread_cond_clockwait). Restricted to Linux via a
case on uname so the Windows path is unchanged.

After both changes: 177/177 URLab.* automation tests still pass on
Linux from a clean third-party rebuild.

Refs: URLab-Sim#45
Replace the "Linux is experimental" line in the README requirements
with a pointer to docs/linux_setup.md, mention the Linux build
script alongside the Windows one in the install section, and add
the new doc to the mkdocs nav under Guides.

The new docs/linux_setup.md captures:

- Prerequisites: UE 5.7 Linux binary, CMake 3.24+, host UE5 C++
  project with the plugin in Plugins/.
- Why a special build flow: UE's bundled clang + libc++ require
  ABI-compatible third-party libs, so the system gcc + libstdc++
  default of build_all.sh produces unlinkable binaries.
- The full env-var sandwich (CC/CXX/AR/RANLIB/CFLAGS/CXXFLAGS/LDFLAGS)
  pointing the third-party CMake builds at UE's clang.
- The role of Scripts/setup_runtime_linux.sh in staging the third-
  party .so files into Binaries/Linux/ so $ORIGIN-RPATH resolves
  them without LD_LIBRARY_PATH.
- How to run the automation suite via Scripts/build_and_test_linux.sh,
  including the editor-must-be-closed gotcha.
- A short list of known caveats (plugin must be inside the host
  project's Plugins/, first-launch shader compile time, etc.).

Refs: URLab-Sim#45
Accept --engine <UE_ROOT> on third_party/build_all.sh. When given on
Linux, glob Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64/
v*_clang-*/x86_64-unknown-linux-gnu so the script survives UE version
bumps (v26 -> v27) without a script edit, then export
CC / CXX / AR / RANLIB and the matching CFLAGS / CXXFLAGS / LDFLAGS
internally so child build.sh invocations inherit them.

Replaces the explicit env-var sandwich docs/linux_setup.md previously
asked the user to type. The new flow is a single command:

    ./third_party/build_all.sh --engine $UE_ROOT

Without --engine the script falls through to the host toolchain
(unchanged behaviour for Windows / macOS, and for any Linux user who
deliberately wants the system gcc + libstdc++).

Why this matters: with the wrong toolchain, system gcc + libstdc++
either produces .so files whose C++ ABI doesn't match UE's plugin
link (wall of std::* undefined-symbol errors), or — worse, with some
combinations — emits LTO bitcode objects that masquerade as .so but
fail to load with a cryptic "file too short" at editor startup.
Both are real failure modes that cost real debugging time.
Each MuJoCo / CoACD / libzmq build.sh now invokes
Scripts/setup_runtime_linux.sh after install on Linux. This catches
the partial-rebuild case: bumping a single submodule SHA and running
only that dep's build.sh (skipping build_all.sh and
build_and_test_linux.sh) would otherwise leave Binaries/Linux/ holding
stale symlinks pointing into the wiped install/<dep>/lib/ — the editor
fails to load with no obvious cause.

setup_runtime_linux.sh now warn-and-skips with exit 0 when
Binaries/Linux/ doesn't exist yet, so a first-time fresh checkout
(third-party built before the plugin .so) doesn't error out. The next
plugin build creates Binaries/Linux/, the next setup_runtime_linux.sh
call (from any of the build paths) populates it.

Linux-only block — guarded with `if [ "$(uname -s)" = "Linux" ]` —
so Windows / macOS paths are unchanged.
linux_setup.md previously assumed a working $UE_ROOT. Add a short
"Unreal Engine 5" subsection at the top of Prerequisites covering:

- where to get the precompiled Linux binary (epicgames.com/linux)
- the .zip flow (~25 GB compressed, ~43 GB extracted)
- disk-space rule of thumb (~70 GB free for initial setup including
  shader / DDC cache)
- display options for headless servers (NICE DCV / X2Go / VNC)
- common apt prereqs (libsdl2, libvulkan1) for fresh Ubuntu 22.04

Mentions the source-build path exists but is not required (most
users land on this page from a fresh distro and don't realise the
binary distribution exists).
@vivekgr92
Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed updates addressing all four:

  1. Rebased onto current main — no conflicts. Mirrored 85092a51's host/path scrub, deps line, and auto-target derivation into build_and_test_linux.sh so the two scripts stay in step.
  2. build_all.sh --engine $UE_ROOT now folds the env-var sandwich internally. Globs v*_clang-* under the SDK base so future UE bumps (v26 → v27) survive without a script edit. Without --engine, behaviour is unchanged for Windows / macOS / any Linux user who deliberately wants the host toolchain. docs/linux_setup.md collapses to a single ./build_all.sh --engine $UE_ROOT step.
  3. Per-dep build.sh auto-stages Binaries/Linux/ — each MuJoCo / CoACD / libzmq build.sh invokes Scripts/setup_runtime_linux.sh after install on Linux. The helper warn-skips with exit 0 when Binaries/Linux/ doesn't exist yet, so first-time fresh checkouts (third-party built before the plugin .so) don't break.
  4. Upstream CoACD PR sent: Use #if defined(_WIN32) instead of #if _WIN32 in public/coacd.h SarahWeiii/CoACD#101 — single-line #if _WIN32#if defined(_WIN32). Once it lands and the CoACD pin moves, the CoACD_custom/public/ overlay can be dropped entirely.

Also folded in your follow-up flag (#5): docs/linux_setup.md now has a UE5-on-Linux installation prerequisites section covering the precompiled binary download, disk-space rule of thumb, display options for headless servers, and the libsdl2 / libvulkan1 apt prereqs that fresh Ubuntu 22.04 sometimes needs.

Build + test summary, post-rebase + post-changes:

=== URLab build+test summary ===
Timestamp : 2026-04-28 17:47:00 UTC
Git HEAD  : acbdd385 (linux-support)
Engine    : UnrealEngine
Deps      : mj=72cb2b2 coacd=c7436bf zmq=7d95ac0
Build     : Succeeded
Tests     : 177 / 177 passed (0 failed)  [177 tests performed]
Log sha256: 320f3fd5c74cd4f2
================================

Ready for another look.

Copy link
Copy Markdown
Contributor

@jonathanembleyriches jonathanembleyriches left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for turning these around so quickly!

One question before merge: have you packaged a host project on Linux (RunUAT.sh BuildCookRun ... -package -stage) and then launched the resulting binary to confirm MuJoCo loads and runs correctly? The Build.cs declarations look right (RuntimeDependencies.Add("$(BinaryOutputDir)/...", LibFile, NonUFS) for the lib branch), but worth a runtime smoke test rather than just a successful package step.

I've left two inline fix suggestions and a few nitpicks.

Thanks for your contribution!

Comment thread docs/linux_setup.md
Comment thread docs/linux_setup.md Outdated
Comment thread Source/URLab/Private/MuJoCo/Core/MjPhysicsEngine.cpp Outdated
Comment thread third_party/build_all.sh Outdated
Per review feedback: Linux no longer adds the third-party .so to
PublicDelayLoadDLLs (RPATH staging supersedes the delay-load
workaround), so URLab_InstallMujocoCallbacks doesn't need to
GetDllHandle("libmujoco.so.X.Y.Z") + GetDllExport at all on Linux.
The mju_user_error / mju_user_warning symbols are declared
`MJAPI extern void (*...)(const char*)` in mujoco.h and link
directly through the .so we're already linked against, so direct
assignment is enough.

Drops the hardcoded libmujoco.so.3.7.0 SONAME literal — one fewer
spot to update on the next MuJoCo pin bump.

Windows path is unchanged: still GetDllHandle-based because
mujoco.dll genuinely is delay-loaded there.
Per review feedback: bash globs sort lexicographically, so
`for cand in $SDK_BASE/v*_clang-*/...; do ... break; done` picks
v25 over v26 when both exist. Latent today since UE ships exactly
one Linux toolchain per engine version, but it would silently
regress on a future UE bump if a transitional engine ever shipped
two.

Replace the loop with `ls -d ... | sort -V | tail -1` (version-aware
sort, picks the highest) and keep the executability check.
Per review feedback on docs/linux_setup.md:

1. Add a source-build option (UnrealEngine clone + Setup.sh +
   GenerateProjectFiles.sh + make UnrealEditor) under Prerequisites.
   The precompiled binary is still recommended as the path of least
   friction; the source build is for engine-level debugging.

2. Split the doc into clearly-labelled sections:
   - One-time setup (the existing 4-step walkthrough)
   - Day-to-day workflow (just `git pull` + build_and_test_linux.sh)
   - Troubleshooting / Advanced (build flags reference, env-var
     sandwich for manual per-dep rebuilds, known caveats).

   The Reviewer's read was that the doc currently looked like a
   first-setup guide with no signposting for what the loop is after
   that. Now there's a header pointer at the top and the day-to-day
   section is its own anchor.

3. Move the "Build flags applied internally" reference list out of
   step 1 (where most users won't need it) and into Troubleshooting /
   Advanced — it's debugging context, not setup material.

No content removed, just reshelved.
@vivekgr92
Copy link
Copy Markdown
Contributor Author

vivekgr92 commented Apr 29, 2026

Thanks for the careful review. All four addressed:

Inline fixes

1. MjPhysicsEngine.cpp direct-assign on Linux2b22ddb. Wrapped the GetDllHandle / GetDllExport block in #if PLATFORM_WINDOWS and dropped to plain mju_user_error = ... / mju_user_warning = ... everywhere else. No more hardcoded libmujoco.so.3.7.0 literal. Confirmed in the packaged-build log below: LogURLab: [URLab] MuJoCo error callbacks installed (direct).

2. build_all.sh toolchain selection10d7276. Replaced the bash glob loop with ls -d ... | sort -V | tail -1 so the highest-numbered toolchain wins on a future UE bump, not the lowest. Verified locally — picks v26_clang-20.1.8-rockylinux8 as expected.

Doc nitpicks

3. linux_setup.md restructurebcb3df4. Three changes:

  • Added a source-build option (UnrealEngine clone + Setup.sh + make UnrealEditor) under Prerequisites, alongside the precompiled binary. Notes that source is for engine-level debugging.
  • Split the doc into clearly-labelled sections: One-time setup (the existing 4-step walkthrough), Day-to-day workflow (just git pull + build_and_test_linux.sh), and Troubleshooting / Advanced.
  • Moved the build-flags reference list from step 1 (where most users won't need it) into Troubleshooting / Advanced. Also added a "build one dep manually" subsection there with the env-var sandwich for anyone iterating on a single dep.

Packaging smoke test

Packaged the host project on Linux and confirmed the binary loads MuJoCo at runtime:

$UE_ROOT/Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \
  -project=/path/to/HostProject.uproject \
  -platform=Linux -clientconfig=Development \
  -nop4 -build -cook -stage -package \
  -archive -archivedirectory=/path/to/Packaged

Build ran clean (BuildCookRun time: 113.82 s, ExitCode=0 (Success)). The third-party libs end up at Packaged/Linux/<Project>/Binaries/Linux/:

libmujoco.so.3.7.0      (real file)
libmujoco.so            (symlink)
libzmq.so.5.2.6         (real file)
libzmq.so.5             (symlink)
libzmq.so               (symlink)
lib_coacd.so

Launched the packaged binary with LD_LIBRARY_PATH unset, DISPLAY=:1. Relevant log lines:

LogURLab: [URLab] MuJoCo error callbacks installed (direct)
LogURLab: Loaded libmujoco.so.3.7.0 from .../Binaries/Linux/libmujoco.so.3.7.0
LogURLab: Loaded libzmq.so.5        from .../Binaries/Linux/libzmq.so.5
LogURLab: Loaded lib_coacd.so       from .../Binaries/Linux/lib_coacd.so

So the RuntimeDependencies.Add("$(BinaryOutputDir)/...", LibFile, NonUFS) declarations stage correctly through BuildCookRun, and ${ORIGIN} RPATH (which UBT correctly adds for packaged binaries) resolves the libs without needing setup_runtime_linux.sh post-package.

Editor regression check

Re-ran the automation suite after the three fixes, still 177 / 177 on Linux:

=== URLab build+test summary ===
Timestamp : 2026-04-29 22:26:53 UTC
Git HEAD  : 0993efef (linux-support)
Engine    : UnrealEngine
Deps      : mj=72cb2b2 coacd=c7436bf zmq=7d95ac0
Build     : Succeeded
Tests     : 177 / 177 passed (0 failed)  [177 tests performed]
Log sha256: cb2bfe0157f9065d
================================

(Summary is from before the three new commits; will re-run after this push and update if anything regressed, but the fixes are scoped narrowly enough I'd be surprised.)

Let me know if there is anything that I missed... I hope not. Thank you for your patience!

Per the second half of jonathanembleyriches's nitpick on
linux_setup.md: setup_runtime_linux.sh is now invoked
automatically by every per-dep build.sh and by
build_and_test_linux.sh, so users never need to run it manually
during normal setup or iteration. Keeping it as Step 3 of the
one-time setup walkthrough was misleading.

Removed it from the One-time setup section. Added a "How runtime
staging works" subsection in Troubleshooting / Advanced that
explains what the script does, when it auto-fires, and how to
run it manually in the rare case you need to. Renumbered "Launch
the editor" from step 4 to step 3.

The brief mention is preserved at the end of step 2 for users who
want the link to the explanation, but the action is gone.
@vivekgr92
Copy link
Copy Markdown
Contributor Author

Quick follow-up — re-reading my previous reply, I missed the first half of your line-83 nitpick. I had moved the build-flags reference list to Troubleshooting / Advanced, but I left Step 3 "Stage runtime libs" sitting in the One-time setup walkthrough even though setup_runtime_linux.sh is now invoked automatically by every per-dep build.sh and by build_and_test_linux.sh — so users never run it manually under the normal flow.

Fixed in 6cba181:

  • Removed Step 3 from One-time setup; renumbered "Launch the editor" 4 → 3.
  • Added a How runtime staging works subsection in Troubleshooting / Advanced that explains what the script does, when it auto-fires (per-dep build, build_and_test_linux.sh), and how to invoke it manually in the rare case it's needed.
  • Kept a short pointer at the end of step 2 linking to the Troubleshooting subsection so the explanation is one click away.

Sorry for the half-fix.

@vivekgr92
Copy link
Copy Markdown
Contributor Author

Re-ran the suite at the latest HEAD to make sure the four review-addressing commits didn't regress anything:

=== URLab build+test summary ===
Timestamp : 2026-04-29 23:01:15 UTC
Git HEAD  : 6cba181e (linux-support)
Engine    : UnrealEngine
Deps      : mj=72cb2b2 coacd=c7436bf zmq=7d95ac0
Build     : Succeeded
Tests     : 177 / 177 passed (0 failed)  [177 tests performed]
Log sha256: 9d52b455447a13bc
================================

Still green.

Copy link
Copy Markdown
Contributor

@jonathanembleyriches jonathanembleyriches left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work. Thanks for the contribution!

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Linux support: build, runtime, and importer issues (172/177 tests passing)

2 participants